/******************************************************************************
* @file    	spi_flash.c
* @author 	dralee
* @version	v1.0
* @date			2021-12-08
* @brief		SPI操作FLASH（W25Q64）
*				W25Q64支持SPI模式0及模式3，支持双线全双工，
*		使用MSB先行模式，支持最高通讯时钟为104MHz，数据
*		帧长度为8位。
********************************************************************************/
#include "spi_flash.h"

static __IO uint32_t SPITimeOut = SPIT_LONG_TIMEOUT;
static uint16_t SPI_TIMEOUT_UserCallback(uint8_t errCode);

/**
********************************************************************************
* @brief SPI FLASH初始化
* @param 无
* @retval 无
********************************************************************************/
void SPI_FLASH_Init(void)
{
	SPI_InitTypeDef SPI_InitStruct;
	GPIO_InitTypeDef GPIO_InitStruct;
	
	FLASH_SPI_APBxClock_FUN(FLASH_SPI_CLK, ENABLE); 					// 使能SPI时钟
	FLASH_SPI_CS_APBxClock_FUN(FLASH_SPI_CS_CLK, ENABLE);			// 使能SPI CS片选引脚时钟
	FLASH_SPI_SCK_APBxClock_FUN(FLASH_SPI_SCK_CLK, ENABLE);		// 使能SPI SCLK时钟
	FLASH_SPI_MISO_APBxClock_FUN(FLASH_SPI_MISO_CLK, ENABLE);	// 使能SPI MISO时钟
	FLASH_SPI_MOSI_APBxClock_FUN(FLASH_SPI_MOSI_CLK, ENABLE); // 使能SPI MOSI时钟
	
	// 配置SPI引脚
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;							// 推挽输出
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStruct.GPIO_Pin = FLASH_SPI_CS_PIN;							// SPI的CS引脚，普通引脚即可
	GPIO_Init(FLASH_SPI_CS_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Pin = FLASH_SPI_SCK_PIN;							// SPI的SCK引脚
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;							// 复用推挽输出
	GPIO_Init(FLASH_SPI_SCK_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Pin = FLASH_SPI_MISO_PIN;							// SPI的MISO引脚
	GPIO_Init(FLASH_SPI_MISO_PORT, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Pin = FLASH_SPI_MOSI_PIN;							// SPI的MOSI引脚
	GPIO_Init(FLASH_SPI_MOSI_PORT, &GPIO_InitStruct);
	
	SPI_FLASH_CS_HIGH();				// 停止信号：FLASH CS引脚高电平
	
	// SPI模式配置，设置了模式3
	SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 时钟分频
	SPI_InitStruct.SPI_CPHA = SPI_CPHA_2Edge;												// 相位：偶数边沿
	SPI_InitStruct.SPI_CPOL = SPI_CPOL_High;												// 时钟极性：高电平
	SPI_InitStruct.SPI_CRCPolynomial = 7;
	SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;									// 8位
	SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	// 双线全双工
	SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;									// MSB先行模式
	SPI_InitStruct.SPI_Mode = SPI_Mode_Master;											// 主设
	SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;													// 软件片选
	SPI_Init(FLASH_SPIx, &SPI_InitStruct);
	
	SPI_Cmd(FLASH_SPIx, ENABLE);	// 使能SPI
}

/**
********************************************************************************
* @brief 擦除FLASH扇区
* @param sectorAddr：要擦除的扇区地址
* @retval 无
********************************************************************************/
void SPI_FLASH_SectorErase(uint32_t sectorAddr)
{	
	// 发送FLASH写使能命令
	SPI_FLASH_WriteEnable();
	SPI_FLASH_WaitForWriteEnd();	
	SPI_FLASH_CS_LOW();																// 擦除扇区，选择FLASH：CS低电平
	SPI_FLASH_SendByte(W25X_SectorErase); 						// 发送扇区擦除指令
	SPI_FLASH_SendByte((sectorAddr & 0xFF0000)>>16);	// 发送擦除扇区地址的高位
	SPI_FLASH_SendByte((sectorAddr & 0xFF00) >> 8);		// 发送擦除扇区地址的中位
	SPI_FLASH_SendByte(sectorAddr & 0xFF);						// 发磅擦除扇区地址的低位
	SPI_FLASH_CS_HIGH();															// 停止信号，FLASH：CS高电平
	SPI_FLASH_WaitForWriteEnd();											// 等待擦除完毕
}

/**
********************************************************************************
* @brief 擦除FLASH扇区，整片擦除
* @param sectorAddr：要擦除的扇区地址
* @retval 无
********************************************************************************/
void SPI_FLASH_BulkErase(void)
{
	SPI_FLASH_WriteEnable();						// 发送FLASH写使能命令
	SPI_FLASH_CS_LOW();									// 整块Erase，选择FLASH：CS低电平
	SPI_FLASH_SendByte(W25X_ChipErase);	// 发送整块擦除指令
	SPI_FLASH_CS_HIGH();								// 停止信号 FLASH：CS高电平
	SPI_FLASH_WaitForWriteEnd();				// 等待擦除完毕
}

/**
********************************************************************************
* @brief 对FLASH按页写入数据，调用本函数前需要先擦除扇区
* @param pBuffer：要写入的数据指针
* @param writeAddr：写入地址
* @param numByteToWrite：写入数据长度，必须小于等于SPI_FLASH_PerWritePageSize
* @retval 无
********************************************************************************/
void SPI_FLASH_PageWrite(uint8_t *pBuffer, uint32_t writeAddr, uint16_t numByteToWrite)
{
	SPI_FLASH_WriteEnable();													// 发送FLASH使能命令
	SPI_FLASH_CS_LOW();																// 选择FLASH CS低电平
	SPI_FLASH_SendByte(W25X_PageProgram);							// 写页写指令
	SPI_FLASH_SendByte((writeAddr & 0xFF0000) >> 16);	// 发送写地址高位
	SPI_FLASH_SendByte((writeAddr & 0xFF00) >> 8);		// 发送写地址中位
	SPI_FLASH_SendByte(writeAddr & 0xFF);							// 发送写地址低位
	
	if(numByteToWrite > SPI_FLASH_PerWritePageSize)
	{
		numByteToWrite = SPI_FLASH_PerWritePageSize;
		FLASH_ERROR("SPI_FLASH_PageWrite too large!");
	}
	// 写入数据
	while(numByteToWrite--)
	{
		SPI_FLASH_SendByte(*pBuffer);	// 发送当前要写入字节数据
		pBuffer++; // 下一字节指针
	}
	
	SPI_FLASH_CS_HIGH();															// 停止信号 FLASH CS高电平
	SPI_FLASH_WaitForWriteEnd();											// 等待写入完毕
}

/**
********************************************************************************
* @brief 对FLASH按页写入数据，调用本函数前需要先擦除扇区
* @param pBuffer：要写入的数据指针
* @param writeAddr：写入地址
* @param numByteToWrite：写入数据长度
* @retval 无
********************************************************************************/
void SPI_FLASH_BufferWrite(uint8_t *pBuffer, uint32_t writeAddr, uint16_t numByteToWrite)
{
	uint8_t numOfPage = 0, numOfSingle = 0, addr = 0, count = 0, temp = 0;
	addr = writeAddr % SPI_FLASH_PageSize;	// mod求余，若writeAddr是SPI_FLASH_PageSize整数倍，则addr=0
	count = SPI_FLASH_PageSize - addr; // 差count个数据值，刚好对齐到页地址
	numOfPage = numByteToWrite / SPI_FLASH_PageSize; // 计算出要写多少个整数页
	numOfSingle = numByteToWrite % SPI_FLASH_PageSize; // mod求余，计算出剩余不满一页的字节数
	
	if(addr == 0) // writeAddr刚好按页对齐 aligned
	{
		if(numOfPage == 0) // numByteToWrite < SPI_FLASH_PageSize，即不满1页
		{
			SPI_FLASH_PageWrite(pBuffer, writeAddr, numByteToWrite);
		}
		else // numByteToWrite > SPI_FLASH_PageSize，即大于1页
		{
			while(numOfPage--) // 先把整数页都写了
			{
				SPI_FLASH_PageWrite(pBuffer, writeAddr, SPI_FLASH_PageSize);
				pBuffer += SPI_FLASH_PageSize;	// 下一页位置
				writeAddr += SPI_FLASH_PageSize;
			}
			
			if(numOfSingle > 0) // 若还有剩余不满1页的数据，写完它
			{
				SPI_FLASH_PageWrite(pBuffer, writeAddr, numOfSingle); // 若还有满一页的数据，写完它
			}
		}
	}
	else // writeAddr非按页（SPI_FLASH_PageSize）对齐，不对齐
	{
		if(numOfPage == 0) // numByteToWrite < SPI_FLASH_PageSize，即不满1页
		{
			if(numOfSingle > count) // 当前页剩余count个位置比numOfSingle小，一页写不完
			{
				temp = numOfSingle - count;
				SPI_FLASH_PageWrite(pBuffer, writeAddr, count); // 先写满当前页
				
				writeAddr += count; // 移到下一位置
				pBuffer += count;
				SPI_FLASH_PageWrite(pBuffer, writeAddr, temp); // 写剩余数据
			}
			else // 当前页剩余count个位置能写完numOfSingle个数据
			{
				SPI_FLASH_PageWrite(pBuffer, writeAddr, numOfSingle);
			}
		}
		else // numByteToWrite > SPI_FLASH_PageSize，即大于1页
		{
			numByteToWrite -= count; // 埴不对齐多出的count个分别处理，不加入这个运算
			numOfPage = numByteToWrite / SPI_FLASH_PageSize;
			numOfSingle = numByteToWrite % SPI_FLASH_PageSize;
			
			SPI_FLASH_PageWrite(pBuffer, writeAddr, count); // 先写完count个数据，为了是让下一次要写的地址对齐
			
			writeAddr += count;			// 下一位置，接下来就重复地址对齐的情况
			pBuffer += count;
			
			while(numOfPage--) // 把整数页都写了
			{
				SPI_FLASH_PageWrite(pBuffer, writeAddr, SPI_FLASH_PageSize);
				writeAddr += SPI_FLASH_PageSize;
				pBuffer += SPI_FLASH_PageSize;
			}
			if(numOfSingle > 0) // 若还有剩余不满1页的数据，写完它
			{
				SPI_FLASH_PageWrite(pBuffer, writeAddr, numOfSingle);
			}
		}
	}
}

/**
********************************************************************************
* @brief 读取FLASH数据
* @param pBuffer：存储读出数据指针
* @param readAddr：读取地址
* @param numByteToRead：读取数据长度
* @retval 无
********************************************************************************/
void SPI_FLASH_BufferRead(uint8_t *pBuffer, uint32_t readAddr, uint16_t numByteToRead)
{
	SPI_FLASH_CS_LOW();																// 选择FLASH： CS低电平
	SPI_FLASH_SendByte(W25X_ReadData);								// 发送读指令
	SPI_FLASH_SendByte((readAddr & 0xFF0000) >> 16);	// 发送读地址高位
	SPI_FLASH_SendByte((readAddr & 0xFF00) >> 8);			// 发送读地址中位
	SPI_FLASH_SendByte(readAddr & 0xFF);							// 发送读地址低位
	
	// 读取数据
	while(numByteToRead--)
	{
		*pBuffer = SPI_FLASH_ReadByte();	// 读取一个字节
		pBuffer++;												// 下一字节缓冲区
	}
	SPI_FLASH_CS_HIGH();								// 停止信号 FLASH CS高电平
}

/**
********************************************************************************
* @brief 读取FLASH ID
* @param 无
* @retval 无
********************************************************************************/
uint32_t SPI_FLASH_ReadID(void)
{
	uint32_t temp = 0, temp0 = 0, temp1 = 0, temp2 = 0;
	SPI_FLASH_CS_LOW();											// 开始通讯 CS低电平
	SPI_FLASH_SendByte(W25X_JedecDeviceID);	// 发送JEDEC指令，读取ID
	temp0 = SPI_FLASH_SendByte(Dummy_Byte);	// 读取一个字节数据  返回由高位->低位，24位
	temp1 = SPI_FLASH_SendByte(Dummy_Byte);	// 读取一个字节数据
	temp2 = SPI_FLASH_SendByte(Dummy_Byte);	// 读取一个字节数据
	SPI_FLASH_CS_HIGH();										// 停止通讯 CS高电平
	
	temp = (temp0 << 16) | (temp1 << 8) | temp2; // 把数据组合起来，作为函数返回值
	return temp;
}

/**
********************************************************************************
* @brief 读取FLASH Device ID
* @param 元
* @retval 无
********************************************************************************/
uint32_t SPI_FLASH_ReadDeviceID(void)
{
	uint32_t temp;
	SPI_FLASH_CS_LOW();									// 开始通讯 FLASH CS低电平
	SPI_FLASH_SendByte(W25X_DeviceID);	// 发送"RDID" 指令
	SPI_FLASH_SendByte(Dummy_Byte);			// 发送一个字节
	SPI_FLASH_SendByte(Dummy_Byte);			// 发送一个字节
	SPI_FLASH_SendByte(Dummy_Byte);			// 发送一个字节
	
	temp = SPI_FLASH_SendByte(Dummy_Byte);	// 读取1个字节
	SPI_FLASH_CS_HIGH();								// 停止通讯 FLASH CS高电平
	return temp;
}

/**
********************************************************************************
* @brief 开始读取序列
*			从闪存启动读取数据字节（读取）序列。这是通过将/CS线路拉低以选择设备来实现的，
*			然后传输读取指令，后跟3个字节地址此功能退出并将/CS线路保持在低位，以便
*			仍在选择闪存。用这种技术，整个闪存的内容通过单个读取指令读取。
* @param writeAddr：读取地址
* @retval 无
********************************************************************************/
void SPI_FLASH_StartReadSequence(uint32_t readAddr)
{
	SPI_FLASH_CS_LOW();																// 开始通讯 FLASH CS低电平
	SPI_FLASH_SendByte(W25X_ReadData);								// 发送“读内存”指令
	SPI_FLASH_SendByte((readAddr & 0xFF0000) >> 16);	// 发送24位地址高8位
	SPI_FLASH_SendByte((readAddr & 0xFF00) >> 8);			// 发送24位地址中8位
	SPI_FLASH_SendByte(readAddr & 0xFF);							// 发送24位地址低8位
}

/**
********************************************************************************
* @brief 进入掉电模式
* @param 无
* @retval 无
********************************************************************************/
void SPI_FLASH_PowerDown(void)
{
	SPI_FLASH_CS_LOW();									// 通讯开始 FLASH CS低电平
	SPI_FLASH_SendByte(W25X_PowerDown);	// 发送掉电命令
	SPI_FLASH_CS_HIGH();								// 通讯结束 FLASH CS高电平
}

/**
********************************************************************************
* @brief 唤醒
* @param 无
* @retval 无
********************************************************************************/
void SPI_FLASH_WakeUp(void)
{
	SPI_FLASH_CS_LOW();													// 通讯开始 FLASH CS低电平
	SPI_FLASH_SendByte(W25X_ReleasePowerDown);	// 发送“上电”命令
	SPI_FLASH_CS_HIGH();												// 通讯结束 FLASH CS高电平
}

	/**
********************************************************************************
* @brief 使用SPI读取一个字节数据
* @param 无
* @retval 返回接收到数据
********************************************************************************/
uint8_t SPI_FLASH_ReadByte(void)
{	
	return (SPI_FLASH_SendByte(Dummy_Byte));
}

/**
********************************************************************************
* @brief 使用SPI发送一个字节数据
* @param byte：要发送的数据
* @retval 返回接收到的数据
********************************************************************************/
uint8_t SPI_FLASH_SendByte(uint8_t byte)
{
	SPITimeOut = SPIT_FLAG_TIMEOUT;
	while(SPI_I2S_GetFlagStatus(FLASH_SPIx, SPI_I2S_FLAG_TXE) == RESET) // 等待发送缓冲区为空，TXE事件
	{
		if((SPITimeOut--) == 0) return SPI_TIMEOUT_UserCallback(0);
	}
	SPI_I2S_SendData(FLASH_SPIx, byte); // 写入数据寄存器，把要写入的数据写入发送缓冲区
	SPITimeOut = SPIT_FLAG_TIMEOUT;
	while(SPI_I2S_GetFlagStatus(FLASH_SPIx, SPI_I2S_FLAG_RXNE) == RESET) // 等待接收缓冲区非空，RXNE事件
	{
		if((SPITimeOut--) == 0) return SPI_TIMEOUT_UserCallback(1);
	}
	return SPI_I2S_ReceiveData(FLASH_SPIx); // 读取数据寄存器，获取接收缓冲区数据
}

/**
********************************************************************************
* @brief 使用SPI发送两个字节数据
* @param halfWord：要发送的数据
* @retval 返回接收到数据
********************************************************************************/
uint16_t SPI_FLASH_SendHalfWord(uint16_t halfWord)
{
	SPITimeOut = SPIT_FLAG_TIMEOUT;
	while(SPI_I2S_GetFlagStatus(FLASH_SPIx, SPI_I2S_FLAG_TXE) == RESET) // 等待发送缓冲区为空，TXE事件
	{
		if((SPITimeOut--)==0) SPI_TIMEOUT_UserCallback(2);
	}
	SPI_I2S_SendData(FLASH_SPIx, halfWord); // 写入数据寄存器，把要写入的数据写入发送缓冲区
	
	SPITimeOut = SPIT_FLAG_TIMEOUT;
	while(SPI_I2S_GetFlagStatus(FLASH_SPIx, SPI_I2S_FLAG_RXNE) == RESET) // 等待接收缓冲区非空，RXNE事件
	{
		if((SPITimeOut--)==0) SPI_TIMEOUT_UserCallback(3);
	}
	return SPI_I2S_ReceiveData(FLASH_SPIx); // 读取数据寄存器，获取接收缓冲区数据
}

/**
********************************************************************************
* @brief 向FLASH发送 写使能 命令
* @param 无
* @retval 无
********************************************************************************/
void SPI_FLASH_WriteEnable(void)
{
	SPI_FLASH_CS_LOW();											// 通讯开始 FLASH CS低电平
	SPI_FLASH_SendByte(W25X_WriteEnable);		// 发送写使能命令
	SPI_FLASH_CS_HIGH();										// 通讯结束 FLASH CS高电平
}

#define WIP_Flag				0x01		// WIP(busy)标志，FLASH内部正在写入

/**
********************************************************************************
* @brief 等待WIP(BUSY)标志被置0，即等待到FLASH内部数据写入完毕
* @param 无
* @retval 无
********************************************************************************/
void SPI_FLASH_WaitForWriteEnd(void)
{
	uint8_t	flashStatus = 0;
	SPI_FLASH_CS_LOW();											// 开始通讯 FLASH CS低电平
	SPI_FLASH_SendByte(W25X_ReadStatusReg);	// 发送“读状态寄存器”命令
	do // 若FLASH忙碌，则等待
	{
		flashStatus = SPI_FLASH_SendByte(Dummy_Byte);
	}while((flashStatus & WIP_Flag) == SET); // 正在写入标志
		
	SPI_FLASH_CS_HIGH();										// 通讯结束 FLASH CS高电平
}

/**
********************************************************************************
* @brief 等待超时回调函数
* @param 无
* @retval 无
********************************************************************************/
static uint16_t SPI_TIMEOUT_UserCallback(uint8_t errCode)
{
	FLASH_ERROR("SPI等待超时！errCode = %d", errCode); // 等待超时后处理，输出错误信息
	return 0;
}
